# This file is part of GUP.
#
# GUP is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# GUP is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with GUP; if not, write to the Free Software Foundation, Inc., 51
# Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#
# Copyright (C) 2007 Julio Biason

'''Album list storage facility

>>> a = AlbumList()
>>> for album in a.tree():
...    print album
(0, '/', 0)
>>> a.add(7, 'gallery', 0)
>>> for album in a.tree():
...    print album
(0, '/', 0)
(1, 'gallery', 7)
>>> a.add(9, 'toons', 8)
>>> for album in a.tree():
...    print album
(0, '/', 0)
(1, 'gallery', 7)
>>> a.add(8, 'pics', 7)
>>> for album in a.tree():
...    print album
(0, '/', 0)
(1, 'gallery', 7)
(2, 'pics', 8)
(3, 'toons', 9)
'''

import logging

__revision__ = 0.1

class AlbumError(Exception):
    '''Generic exception used in the albums'''
    pass

class AlbumExistsError(AlbumError):
    '''Album exists'''
    pass

class AlbumDoesntExistError(AlbumError):
    '''The requested album doesn't exist'''
    pass

#pylint: disable-msg=R0903
class Album(object):
    '''A single album information'''
    def __init__(self, album_id = '', name = '', parent = None):
        self.album_id = album_id
        self.name     = name
        self.parent   = parent
        self.child    = {}

    def __eq__(self, other):
        '''Equality check.'''
        return self.album_id == other.album_id

    def __cmp__(self, other):
        '''Comparision (used to sort the album names).'''
        if self.name == other.name:
            return 0
        elif self.name < other.name:
            return -1
        return 1

class AlbumList(object):
    '''Album list storage facility'''
    def __init__(self):
        self.start           = Album(0, '/')
        self.list            = {0: self.start}
        self.missing_parents = {}

    def add(self, album_id, album_name, album_parent):
        '''Add an album to the list.
        
        album_id - ID of the album
        album_name - Name of the album
        album_parent - ID of this album parent
        
        The parent doesn't need to exist before adding the album. If the
        parent can't be found, it album will stay on an "orphan" list till
        the parent is added.'''
        logging.debug('Adding album "%s", id "%s", parent "%s"' % \
                (album_name, album_id, album_parent))
        if self.list.has_key(album_id):
            raise AlbumExistsError

        album_info = Album(album_id, album_name, album_parent)

        # add in the tree
        try:
            album_pos = self.list[album_parent]
        except KeyError:
            # if the parent couldn't be found, we add the child
            # in a list, waiting for a parent to appear and pick
            # them up (and take them to the mall)
            logging.debug('Missing parent: %s', album_parent)
            try:
                self.missing_parents[album_parent].append(album_info)
            except KeyError:
                self.missing_parents[album_parent] = [album_info]
            return

        album_pos.child[album_id] = album_info

        # add in the quick search list
        self.list[album_id] = album_info

        # just check if the kids are waiting
        if self.missing_parents.has_key(album_id):
            logging.debug('New album is a parent: %d childs' % \
                    len(self.missing_parents[album_id]))
            for child in self.missing_parents[album_id]:
                self.add(child.album_id, child.name, child.parent)

    def get_name(self, album_id):
        '''Return the album name from the album id.'''
        try:
            self.list[album_id]
        except KeyError:
            raise AlbumDoesntExistError

        return self.list[album_id].name


    def tree(self, level = 0, branch = None):
        '''Return tuples with information about the album list to be
        displayed in a tree fashion. The tuples are in the format (level,
        name, id), where "level" is the tree node level of the album;
        albums with the same level are siblings and albums preceding a
        lower level are child of the previous one.
        
        The parameters "level" and "branch" shouldn't be used directly,
        unless you are completely sure about that.'''
        if branch is None:
            branch = self.start
        yield (level, branch.name, branch.album_id)

        ordered_child = branch.child.values()
        ordered_child.sort()

        # this is SO ugly!
        for child in ordered_child:
            for result in self.tree(level+1, child):
                yield result

    def orphans(self):
        '''Return the list of orphans.'''
        return self.missing_parents

    def save(self, name):
        '''Save the album list.'''
        contents = file(name, 'w')
        for key, value in self.list.iteritems():
            if key == 0:
                continue    # we won't save root as it is always there
            logging.debug('%s:%s:%s' % (value.album_id, value.parent,
                value.name))
            print >> contents, '%d:%d:%s' % \
                    (int(value.album_id), int(value.parent), value.name)
        contents.close()
        return

    def load(self, name):
        '''Load the album list. Can be used to merge two album lists, but 
        it won't make sense, as the remote server won't have one of the
        sets. Also, you can load he same album twice without a problem.'''
        contents = file(name, 'r')
        for line in contents:
            line   = line.strip()
            fields = line.split(':')
            self.add(int(fields[0]), ':'.join(fields[2:]), int(fields[1]))
        contents.close()
        return

    def search_name(self, name):
        '''Search for albums with the specified name. Returns a new
        AlbumList object with the albums that have the searched name and
        their parents. If no album could be found, a
        "AlbumDoesntExistError" is thrown.'''

        # build a list of albums with the searched name
        albums_found = []
        name = name.upper()
        for album_info in self.list.itervalues():
            if name in album_info.name.upper():
                albums_found.append(album_info)

        if len(albums_found) == 0:
            raise AlbumDoesntExistError

        # now we create a new AlbumList and put the albums there. It should
        # take care of building the tree by itself.
        result = AlbumList()
        for album in albums_found:
            try:
                result.add(album.album_id, album.name, album.parent)
            except AlbumExistsError:
                pass

            # add the parents!
            parent = album.parent
            while parent != 0:
                parent_album = self.list[parent]

                try:
                    result.add(parent_album.album_id, parent_album.name,
                            parent_album.parent)
                except AlbumExistsError:
                    pass

                parent = parent_album.parent

        return result

    def search_id(self, album_id):
        '''Search for an album with the specified ID. Returns a new
        AlbumList with the album, its parents and its immediate
        children.'''

        try:
            root_album = self.list[album_id]
        except KeyError:
            raise AlbumDoesntExistError

        result = AlbumList()
        result.add(root_album.album_id, root_album.name, root_album.parent)

        # add the parents
        parent = root_album.parent
        while parent != 0:
            parent_album = self.list[parent]

            try:
                result.add(parent_album.album_id, parent_album.name,
                        parent_album.parent)
            except AlbumExistsError:
                pass

            parent = parent_album.parent

        # add the child nodes
        for child in root_album.child.itervalues():
            try:
                result.add(child.album_id, child.name, child.parent)
            except AlbumExistsError:
                pass

        return result

def _test():
    '''Calls doctest to test this module'''
    import doctest
    doctest.testmod()

if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    _test()
